package com.stripe.android.link.repositories

import android.app.Application
import com.stripe.android.DefaultFraudDetectionDataRepository
import com.stripe.android.core.exception.StripeException
import com.stripe.android.core.frauddetection.FraudDetectionDataRepository
import com.stripe.android.core.injection.IOContext
import com.stripe.android.core.injection.PUBLISHABLE_KEY
import com.stripe.android.core.injection.STRIPE_ACCOUNT_ID
import com.stripe.android.core.networking.ApiRequest
import com.stripe.android.link.LinkPaymentDetails
import com.stripe.android.link.LinkPaymentMethod
import com.stripe.android.link.confirmation.createPaymentMethodCreateParams
import com.stripe.android.link.utils.toConsumerBillingAddress
import com.stripe.android.model.ClientAttributionMetadata
import com.stripe.android.model.ConsumerPaymentDetails
import com.stripe.android.model.ConsumerPaymentDetailsCreateParams
import com.stripe.android.model.ConsumerPaymentDetailsCreateParams.Card.Companion.extraConfirmationParams
import com.stripe.android.model.ConsumerPaymentDetailsUpdateParams
import com.stripe.android.model.ConsumerSession
import com.stripe.android.model.ConsumerSessionLookup
import com.stripe.android.model.ConsumerSessionRefresh
import com.stripe.android.model.ConsumerSessionSignup
import com.stripe.android.model.ConsumerShippingAddresses
import com.stripe.android.model.ConsumerSignUpConsentAction
import com.stripe.android.model.EmailSource
import com.stripe.android.model.IncentiveEligibilitySession
import com.stripe.android.model.LinkAccountSession
import com.stripe.android.model.LinkMode
import com.stripe.android.model.PaymentMethod
import com.stripe.android.model.PaymentMethodCreateParams
import com.stripe.android.model.SharePaymentDetails
import com.stripe.android.model.SignUpParams
import com.stripe.android.model.StripeIntent
import com.stripe.android.model.VerificationType
import com.stripe.android.networking.RequestSurface
import com.stripe.android.networking.StripeRepository
import com.stripe.android.payments.core.analytics.ErrorReporter
import com.stripe.android.repository.ConsumersApiService
import kotlinx.coroutines.withContext
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
import kotlin.coroutines.CoroutineContext

/**
 * Repository that uses [StripeRepository] for Link services.
 */
@SuppressWarnings("TooManyFunctions")
internal class LinkApiRepository @Inject constructor(
    application: Application,
    private val requestSurface: RequestSurface,
    @Named(PUBLISHABLE_KEY) private val publishableKeyProvider: () -> String,
    @Named(STRIPE_ACCOUNT_ID) private val stripeAccountIdProvider: () -> String?,
    private val stripeRepository: StripeRepository,
    private val consumersApiService: ConsumersApiService,
    @IOContext private val workContext: CoroutineContext,
    private val locale: Locale?,
    private val errorReporter: ErrorReporter,
) : LinkRepository {

    private val fraudDetectionDataRepository: FraudDetectionDataRepository =
        DefaultFraudDetectionDataRepository(application, workContext)

    private val apiRequestOptions: ApiRequest.Options
        get() = buildRequestOptions()

    init {
        fraudDetectionDataRepository.refresh()
    }

    override suspend fun lookupConsumer(
        email: String?,
        linkAuthIntentId: String?,
        sessionId: String,
        customerId: String?,
        supportedVerificationTypes: List<String>?,
    ): Result<ConsumerSessionLookup> = withContext(workContext) {
        runCatching {
            requireNotNull(
                consumersApiService.lookupConsumerSession(
                    email = email,
                    linkAuthIntentId = linkAuthIntentId,
                    requestSurface = requestSurface.value,
                    sessionId = sessionId,
                    doNotLogConsumerFunnelEvent = false,
                    requestOptions = apiRequestOptions,
                    customerId = customerId,
                    supportedVerificationTypes = supportedVerificationTypes
                )
            )
        }
    }

    override suspend fun lookupConsumerWithoutBackendLoggingForExposure(
        email: String,
        sessionId: String,
    ): Result<ConsumerSessionLookup> = withContext(workContext) {
        runCatching {
            requireNotNull(
                consumersApiService.lookupConsumerSession(
                    email = email,
                    linkAuthIntentId = null,
                    requestSurface = requestSurface.value,
                    sessionId = sessionId,
                    doNotLogConsumerFunnelEvent = true,
                    supportedVerificationTypes = null,
                    requestOptions = apiRequestOptions,
                    customerId = null
                )
            )
        }
    }

    override suspend fun mobileLookupConsumer(
        email: String?,
        emailSource: EmailSource?,
        linkAuthIntentId: String?,
        verificationToken: String,
        appId: String,
        sessionId: String,
        customerId: String?,
        supportedVerificationTypes: List<String>?,
        linkAuthTokenClientSecret: String?,
    ): Result<ConsumerSessionLookup> = withContext(workContext) {
        runCatching {
            consumersApiService.mobileLookupConsumerSession(
                email = email,
                emailSource = emailSource,
                linkAuthIntentId = linkAuthIntentId,
                requestSurface = requestSurface.value,
                verificationToken = verificationToken,
                appId = appId,
                requestOptions = apiRequestOptions,
                sessionId = sessionId,
                customerId = customerId,
                supportedVerificationTypes = supportedVerificationTypes,
                linkAuthTokenClientSecret = linkAuthTokenClientSecret,
            )
        }
    }

    override suspend fun refreshConsumer(
        appId: String,
        consumerSessionClientSecret: String,
        supportedVerificationTypes: List<String>?,
    ): Result<ConsumerSessionRefresh> = withContext(workContext) {
        runCatching {
            consumersApiService.refreshConsumerSession(
                appId = appId,
                consumerSessionClientSecret = consumerSessionClientSecret,
                supportedVerificationTypes = supportedVerificationTypes,
                requestSurface = requestSurface.value,
                requestOptions = apiRequestOptions,
            )
        }
    }

    override suspend fun consumerSignUp(
        email: String,
        phone: String?,
        country: String?,
        countryInferringMethod: String,
        name: String?,
        consentAction: ConsumerSignUpConsentAction
    ): Result<ConsumerSessionSignup> = withContext(workContext) {
        consumersApiService.signUp(
            SignUpParams(
                email = email,
                phoneNumber = phone,
                country = country,
                countryInferringMethod = countryInferringMethod,
                name = name,
                locale = locale,
                amount = null,
                currency = null,
                incentiveEligibilitySession = null,
                consentAction = consentAction,
                requestSurface = requestSurface.value
            ),
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun mobileSignUp(
        name: String?,
        email: String,
        phoneNumber: String?,
        country: String?,
        countryInferringMethod: String,
        consentAction: ConsumerSignUpConsentAction,
        amount: Long?,
        currency: String?,
        incentiveEligibilitySession: IncentiveEligibilitySession?,
        verificationToken: String,
        appId: String
    ): Result<ConsumerSessionSignup> = withContext(workContext) {
        consumersApiService.mobileSignUp(
            SignUpParams(
                email = email,
                phoneNumber = phoneNumber,
                country = country,
                countryInferringMethod = countryInferringMethod,
                name = name,
                locale = locale,
                amount = amount,
                currency = currency,
                incentiveEligibilitySession = incentiveEligibilitySession,
                consentAction = consentAction,
                requestSurface = requestSurface.value,
                verificationToken = verificationToken,
                appId = appId
            ),
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun createCardPaymentDetails(
        paymentMethodCreateParams: PaymentMethodCreateParams,
        userEmail: String,
        stripeIntent: StripeIntent,
        consumerSessionClientSecret: String,
        clientAttributionMetadata: ClientAttributionMetadata,
    ): Result<LinkPaymentDetails.New> = withContext(workContext) {
        consumersApiService.createPaymentDetails(
            consumerSessionClientSecret = consumerSessionClientSecret,
            paymentDetailsCreateParams = ConsumerPaymentDetailsCreateParams.Card(
                cardPaymentMethodCreateParamsMap = paymentMethodCreateParams.toParamMap(),
                email = userEmail,
            ),
            requestSurface = requestSurface.value,
            requestOptions = apiRequestOptions,
        ).mapCatching {
            val paymentDetails = it.paymentDetails.first()
            val extraParams = extraConfirmationParams(paymentMethodCreateParams.toParamMap())

            val createParams = PaymentMethodCreateParams.createLink(
                paymentDetailsId = paymentDetails.id,
                consumerSessionClientSecret = consumerSessionClientSecret,
                extraParams = extraParams,
                allowRedisplay = paymentMethodCreateParams.allowRedisplay,
                clientAttributionMetadata = clientAttributionMetadata,
            )

            LinkPaymentDetails.New(
                paymentDetails = paymentDetails,
                paymentMethodCreateParams = createParams,
                originalParams = paymentMethodCreateParams,
            )
        }.onFailure {
            errorReporter.report(
                ErrorReporter.ExpectedErrorEvent.LINK_CREATE_PAYMENT_DETAILS_FAILURE,
                StripeException.create(it)
            )
        }
    }

    override suspend fun createBankAccountPaymentDetails(
        bankAccountId: String,
        userEmail: String,
        consumerSessionClientSecret: String,
        clientAttributionMetadata: ClientAttributionMetadata,
    ): Result<ConsumerPaymentDetails.PaymentDetails> = withContext(workContext) {
        consumersApiService.createPaymentDetails(
            consumerSessionClientSecret = consumerSessionClientSecret,
            paymentDetailsCreateParams = ConsumerPaymentDetailsCreateParams.BankAccount(
                bankAccountId = bankAccountId,
                billingAddress = null,
                billingEmailAddress = userEmail,
                clientAttributionMetadata = clientAttributionMetadata.toParams(),
            ),
            requestSurface = requestSurface.value,
            requestOptions = apiRequestOptions,
        ).mapCatching {
            it.paymentDetails.first()
        }.onFailure {
            errorReporter.report(
                ErrorReporter.ExpectedErrorEvent.LINK_CREATE_PAYMENT_DETAILS_FAILURE,
                StripeException.create(it)
            )
        }
    }

    override suspend fun shareCardPaymentDetails(
        paymentMethodCreateParams: PaymentMethodCreateParams,
        id: String,
        consumerSessionClientSecret: String,
        clientAttributionMetadata: ClientAttributionMetadata,
    ): Result<LinkPaymentDetails.Saved> = withContext(workContext) {
        val allowRedisplay = paymentMethodCreateParams.allowRedisplay?.let {
            mapOf(ALLOW_REDISPLAY_PARAM to it.value)
        } ?: emptyMap()
        val billingPhone: Map<String, String> = paymentMethodCreateParams.billingDetails?.phone?.let {
            mapOf("billing_phone" to it)
        } ?: emptyMap()
        val paymentMethodParams = mapOf("expand" to listOf("payment_method"))
        val clientAttributionMetadataParams = clientAttributionMetadata.toParams()

        stripeRepository.sharePaymentDetails(
            consumerSessionClientSecret = consumerSessionClientSecret,
            id = id,
            extraParams = mapOf(
                "payment_method_options" to extraConfirmationParams(paymentMethodCreateParams.toParamMap()),
            ) + allowRedisplay + billingPhone + paymentMethodParams + clientAttributionMetadataParams,
            requestOptions = apiRequestOptions,
        ).onFailure {
            errorReporter.report(ErrorReporter.ExpectedErrorEvent.LINK_SHARE_CARD_FAILURE, StripeException.create(it))
        }.map { paymentMethod ->
            val paymentMethodId = paymentMethod.id
            LinkPaymentDetails.Saved(
                paymentDetails = ConsumerPaymentDetails.Passthrough(
                    id = id,
                    last4 = paymentMethodCreateParams.cardLast4().orEmpty(),
                    paymentMethodId = paymentMethodId,
                    billingEmailAddress = paymentMethod.billingDetails?.email,
                    billingAddress = paymentMethod.billingDetails?.toConsumerBillingAddress(),
                ),
                paymentMethodCreateParams = PaymentMethodCreateParams.createLink(
                    paymentDetailsId = paymentMethodId,
                    consumerSessionClientSecret = consumerSessionClientSecret,
                    extraParams = extraConfirmationParams(paymentMethodCreateParams.toParamMap()),
                    clientAttributionMetadata = clientAttributionMetadata,
                ),
                paymentMethod = paymentMethod,
            )
        }
    }

    override suspend fun sharePaymentDetails(
        consumerSessionClientSecret: String,
        paymentDetailsId: String,
        expectedPaymentMethodType: String,
        billingPhone: String?,
        cvc: String?,
        allowRedisplay: String?,
        apiKey: String?,
        clientAttributionMetadata: ClientAttributionMetadata,
    ): Result<SharePaymentDetails> = withContext(workContext) {
        val fraudParams = fraudDetectionDataRepository.getCached()?.params.orEmpty()
        val paymentMethodParams = mapOf("expand" to listOf("payment_method"))
        val optionsParams = cvc?.let {
            mapOf("payment_method_options" to mapOf("card" to mapOf("cvc" to it)))
        } ?: emptyMap()
        val allowRedisplayParams = allowRedisplay?.let {
            mapOf("allow_redisplay" to allowRedisplay)
        } ?: emptyMap()
        val clientAttributionMetadataParams = clientAttributionMetadata.toParams()
        val extraParams = paymentMethodParams +
            fraudParams +
            optionsParams +
            allowRedisplayParams +
            clientAttributionMetadataParams

        // Allow using a custom API key so that payment methods can be created under the
        // merchant-of-record if necessary.
        val requestOptions = buildRequestOptions(apiKey)

        consumersApiService.sharePaymentDetails(
            consumerSessionClientSecret = consumerSessionClientSecret,
            paymentDetailsId = paymentDetailsId,
            expectedPaymentMethodType = expectedPaymentMethodType,
            requestOptions = requestOptions,
            requestSurface = requestSurface.value,
            extraParams = extraParams,
            billingPhone = billingPhone,
        )
    }

    override suspend fun createPaymentMethod(
        consumerSessionClientSecret: String,
        paymentMethod: LinkPaymentMethod,
        clientAttributionMetadata: ClientAttributionMetadata,
    ): Result<PaymentMethod> = withContext(workContext) {
        val params = createPaymentMethodCreateParams(
            selectedPaymentDetails = paymentMethod.details,
            consumerSessionClientSecret = consumerSessionClientSecret,
            cvc = paymentMethod.collectedCvc,
            billingPhone = paymentMethod.billingPhone,
            clientAttributionMetadata = clientAttributionMetadata,
        )
        stripeRepository.createPaymentMethod(
            paymentMethodCreateParams = params,
            options = apiRequestOptions,
        )
    }

    override suspend fun logOut(
        consumerSessionClientSecret: String,
        consumerAccountPublishableKey: String?,
    ): Result<ConsumerSession> = withContext(workContext) {
        stripeRepository.logOut(
            consumerSessionClientSecret = consumerSessionClientSecret,
            consumerAccountPublishableKey = consumerAccountPublishableKey,
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun startVerification(
        consumerSessionClientSecret: String,
        isResendSmsCode: Boolean
    ): Result<ConsumerSession> {
        return runCatching {
            requireNotNull(
                consumersApiService.startConsumerVerification(
                    consumerSessionClientSecret = consumerSessionClientSecret,
                    locale = locale ?: Locale.US,
                    requestSurface = requestSurface.value,
                    type = VerificationType.SMS,
                    customEmailType = null,
                    connectionsMerchantName = null,
                    requestOptions = apiRequestOptions,
                    isResendSmsCode = isResendSmsCode
                )
            )
        }
    }

    override suspend fun confirmVerification(
        verificationCode: String,
        consumerSessionClientSecret: String,
        consentGranted: Boolean?
    ): Result<ConsumerSession> {
        return runCatching {
            requireNotNull(
                consumersApiService.confirmConsumerVerification(
                    consumerSessionClientSecret = consumerSessionClientSecret,
                    verificationCode = verificationCode,
                    requestSurface = requestSurface.value,
                    type = VerificationType.SMS,
                    consentGranted = consentGranted,
                    requestOptions = apiRequestOptions,
                )
            )
        }
    }

    override suspend fun postConsentUpdate(
        consumerSessionClientSecret: String,
        consentGranted: Boolean,
    ): Result<Unit> = withContext(workContext) {
        consumersApiService.postConsentUpdate(
            consumerSessionClientSecret = consumerSessionClientSecret,
            consentGranted = consentGranted,
            requestSurface = requestSurface.value,
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun listPaymentDetails(
        paymentMethodTypes: Set<String>,
        consumerSessionClientSecret: String,
    ): Result<ConsumerPaymentDetails> {
        return stripeRepository.listPaymentDetails(
            clientSecret = consumerSessionClientSecret,
            paymentMethodTypes = paymentMethodTypes,
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun listShippingAddresses(
        consumerSessionClientSecret: String,
    ): Result<ConsumerShippingAddresses> {
        return stripeRepository.listShippingAddresses(
            clientSecret = consumerSessionClientSecret,
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun deletePaymentDetails(
        paymentDetailsId: String,
        consumerSessionClientSecret: String,
    ): Result<Unit> {
        return stripeRepository.deletePaymentDetails(
            clientSecret = consumerSessionClientSecret,
            paymentDetailsId = paymentDetailsId,
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun updatePaymentDetails(
        updateParams: ConsumerPaymentDetailsUpdateParams,
        consumerSessionClientSecret: String,
    ): Result<ConsumerPaymentDetails> {
        return stripeRepository.updatePaymentDetails(
            clientSecret = consumerSessionClientSecret,
            paymentDetailsUpdateParams = updateParams,
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun createLinkAccountSession(
        consumerSessionClientSecret: String,
        stripeIntent: StripeIntent,
        linkMode: LinkMode?,
    ): Result<LinkAccountSession> {
        return consumersApiService.createLinkAccountSession(
            consumerSessionClientSecret = consumerSessionClientSecret,
            intentToken = stripeIntent.clientSecret,
            linkMode = linkMode,
            requestSurface = requestSurface.value,
            requestOptions = apiRequestOptions,
        )
    }

    override suspend fun updatePhoneNumber(
        consumerSessionClientSecret: String,
        phoneNumber: String,
    ): Result<ConsumerSession> = withContext(workContext) {
        consumersApiService.updatePhoneNumber(
            consumerSessionClientSecret = consumerSessionClientSecret,
            phoneNumber = phoneNumber,
            requestSurface = requestSurface.value,
            requestOptions = apiRequestOptions,
        )
    }

    private fun buildRequestOptions(
        customApiKey: String? = null,
    ): ApiRequest.Options {
        return if (customApiKey != null) {
            ApiRequest.Options(
                apiKey = customApiKey,
                stripeAccount = null,
            )
        } else {
            ApiRequest.Options(
                apiKey = publishableKeyProvider(),
                stripeAccount = stripeAccountIdProvider(),
            )
        }
    }

    private fun ClientAttributionMetadata.toParams(): Map<String, Map<String, Any>> =
        mapOf(CLIENT_ATTRIBUTION_METADATA_PARAM to this.toParamMap())

    private companion object {
        const val ALLOW_REDISPLAY_PARAM = "allow_redisplay"
        const val CLIENT_ATTRIBUTION_METADATA_PARAM = "client_attribution_metadata"
    }
}
